using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using System.Threading;
using System.ComponentModel;

namespace XNAGame
{

    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        //Global variable definitions

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        public GraphicsDevice device;
        float aspectRatio;

        //Terrain data
        public Vector3 vDimensions;
        public Block[] rBlocks;
        public Block[, ,] rBlockMap;
        public int[,] rHeightMap;
        public Chunk[, ,] rChunks;
        public BoundingBox[, ,] rBoundingBoxes;
        public bool[, ,] rBoundingBools;

        //Drawing related vars
        VertexPositionTexture[] sunArray;
        SpriteFont myFont;
        Texture2D tTerrain;
        Texture2D tGui;
        Texture2D tWaterOverlay;
        Texture2D tVignette;
        Texture2D tMoon;
        Texture2D tSun;
        Texture2D tIcons;
        Effect effect;

        //Player var
        Player player;

        //Misc other vars
        int nSpam = 0;
        float fTime = 0.201358f;

        int nTotalFrames = 0;
        float fElapsedTime = 0f;
        int nFps = 0;

        int nLastMouseWheel = 0;
        int nSelectedBlock = 1;

        public void CreateBlockTypes()
        {
            //This method assigns a block to each index of rBlocks
            //It is called in LoadContent()

            //0 - Air
            rBlocks[0] = new Block(new Vector2(), new Vector2(), new Vector2(), new Vector2(), new Vector2(), new Vector2(), false, true, false);
            //1 - Stone
            rBlocks[1] = new Block(new Vector2(1, 0), true, false, true);
            //2 - Grass
            rBlocks[2] = new Block(new Vector2(0, 0), new Vector2(3, 0), new Vector2(3, 0), new Vector2(3, 0), new Vector2(3, 0), new Vector2(2, 0), true, false, true);
            //3 - Dirt
            rBlocks[3] = new Block(new Vector2(2, 0), true, false, true);
            //4 - Cobblestone
            rBlocks[4] = new Block(new Vector2(0, 1), true, false, true);
            //5 - Planks
            rBlocks[5] = new Block(new Vector2(4, 0), true, false, true);
            //6 - Sapling
            rBlocks[6] = new Block(new Vector2(15, 0), false, true, true, 1);
            //7 - Bedrock
            rBlocks[7] = new Block(new Vector2(1, 1), true, false, true);
            //8 - Water
            rBlocks[8] = new Block(new Vector2(13, 12), false, true, false);
            //9 - Water 2
            rBlocks[9] = new Block(new Vector2(13, 12), false, true, false);
            //10 - Lava
            rBlocks[10] = new Block(new Vector2(13, 14), false, false, false);
            //11 - Lava 2
            rBlocks[11] = new Block(new Vector2(13, 14), false, false, false);
            //12 - Sand
            rBlocks[12] = new Block(new Vector2(2, 1), true, false, true);
            //13 - Gravel
            rBlocks[13] = new Block(new Vector2(3, 1), true, false, true);
            //14 - Gold Ore
            rBlocks[14] = new Block(new Vector2(0, 2), true, false, true);
            //15 - Iron Ore
            rBlocks[15] = new Block(new Vector2(1, 2), true, false, true);
            //16 - Coal Ore
            rBlocks[16] = new Block(new Vector2(2, 2), true, false, true);
            //17 - Logs
            rBlocks[17] = new Block(new Vector2(5, 1), new Vector2(4, 1), new Vector2(4, 1), new Vector2(4, 1), new Vector2(4, 1), new Vector2(5, 1), true, false, true);
            //18 - Leaves
            rBlocks[18] = new Block(new Vector2(4, 3), true, true, true);
            //19 - Sponge
            rBlocks[19] = new Block(new Vector2(0, 3), true, false, true);
            //20 - Glass
            rBlocks[20] = new Block(new Vector2(1, 3), true, true, true);
            //21 - Lapis Lazuli Ore
            rBlocks[21] = new Block(new Vector2(0, 10), true, false, true);
            //22 - Lapis Lazuli Block
            rBlocks[22] = new Block(new Vector2(0, 9), true, false, true);
            //23 - Dispenser
            rBlocks[23] = new Block(new Vector2(14, 3), new Vector2(14, 2), new Vector2(13, 2), new Vector2(13, 2), new Vector2(13, 2), new Vector2(2, 0), true, false, true);
            //24 - Sandstone
            rBlocks[24] = new Block(new Vector2(0, 11), new Vector2(0, 12), new Vector2(0, 12), new Vector2(0, 12), new Vector2(0, 12), new Vector2(0, 13), true, false, true);
            //25 - Note Block
            rBlocks[25] = new Block(new Vector2(10, 4), true, false, true);
            //26 - Bed INCOMPLETE
            rBlocks[26] = new Block(new Vector2(1, 4), true, false, true);
            //27 - Powered Rail INCOMPLETE
            rBlocks[27] = new Block(new Vector2(1, 4), true, false, true);
            //28 - Detector Rail INCOMPLETE
            rBlocks[28] = new Block(new Vector2(1, 4), true, false, true);
            //29 - Sticky Piston INCOMPLETE
            rBlocks[29] = new Block(new Vector2(1, 4), true, false, true);
            //30 - Cobweb INCOMPLETE
            rBlocks[30] = new Block(new Vector2(1, 4), true, false, true);
            //31 - Tall Grass INCOMPLETE
            rBlocks[31] = new Block(new Vector2(1, 4), true, false, true);
            //32 - Dead Shrubs INCOMPLETE
            rBlocks[32] = new Block(new Vector2(1, 4), true, false, true);
            //33 - Piston INCOMPLETE
            rBlocks[33] = new Block(new Vector2(1, 4), true, false, true);
            //34 - Piston Extensions INCOMPLETE
            rBlocks[34] = new Block(new Vector2(1, 4), true, false, true);
            //35 - Wool
            rBlocks[35] = new Block(new Vector2(0, 4), true, false, true);
            //36 - Piston Mechanics INCOMPLETE
            rBlocks[36] = new Block(new Vector2(1, 4), true, false, true);
            //37 - Dandelion
            rBlocks[37] = new Block(new Vector2(13, 0), false, true, true, 1);
            //38 - Rose
            rBlocks[38] = new Block(new Vector2(12, 0), false, true, true, 1);
            //39 - Brown Mushroom
            rBlocks[39] = new Block(new Vector2(13, 1), false, true, true, 1);
            //40 - Red Mushroom
            rBlocks[40] = new Block(new Vector2(12, 1), false, true, true, 1);
            //41 - Gold Block
            rBlocks[41] = new Block(new Vector2(7, 1), true, false, true);
            //42 - Iron Block
            rBlocks[42] = new Block(new Vector2(6, 1), true, false, true);
            //43 - Double Slab
            rBlocks[43] = new Block(new Vector2(6, 0), new Vector2(5, 0), new Vector2(5, 0), new Vector2(5, 0), new Vector2(5, 0), new Vector2(6, 0), true, false, true);
            //44 - Slab
            rBlocks[44] = new Block(new Vector2(6, 0), new Vector2(5, 0), new Vector2(5, 0), new Vector2(5, 0), new Vector2(5, 0), new Vector2(6, 0), true, false, true, 2);
            //45 - Brick Block
            rBlocks[45] = new Block(new Vector2(7, 0), true, false, true);
            //46 - TNT
            rBlocks[46] = new Block(new Vector2(9, 0), new Vector2(8, 0), new Vector2(8, 0), new Vector2(8, 0), new Vector2(8, 0), new Vector2(10, 0), true, false, true);
        }

        public void SetBlockMap()
        {
            //This method is called once in LoadContent()
            //It fills the blockmap with block data

            rHeightMap = new int[(int)vDimensions.X, (int)vDimensions.Z]; //This array is only used at the beginning, as it does not update if the data changes. It holds the top layer's heightmap data
            HeightMap floatMap = new HeightMap((int)vDimensions.X); //This is part of the terrain generation, which I don't completely understand
            Random rand = new Random();


            for (int x = 0; x < vDimensions.X; x++)
            {
                for (int z = 0; z < vDimensions.Z; z++)
                {
                    rHeightMap[x, z] = (int)((9f / 20) * vDimensions.Y) + (int)Math.Round((floatMap.Heights[x, z] * 40)); //Set the height map to be 9/20ths of the total height, +/- what the generation says to do
                }
            }

            //This creates rBlockMap from rHeightMap, assigning each position a block based off of certain conditions
            for (int x = 0; x < (int)vDimensions.X; x++)
            {
                for (int z = 0; z < (int)vDimensions.Z; z++)
                {
                    for (int y = 0; y < (int)vDimensions.Y; y++)
                    {
                        if (y > rHeightMap[x, z])
                        {
                            rBlockMap[x, y, z] = rBlocks[0]; //If the position is higher than rHeightMap, then it is air
                        }
                        else if (y == rHeightMap[x, z])
                        {
                            rBlockMap[x, y, z] = rBlocks[2]; //If the position is at the very top of rHeightMap, it is grass
                        }
                        else if (y == 0)
                        {
                            rBlockMap[x, y, z] = rBlocks[7]; //If the position is at the very bottom, it is bedrock
                        }
                        else if (y < 4)
                        {
                            rBlockMap[x, y, z] = rBlocks[(rand.Next(0, 3) == 1 ? 1 : 7)]; //If the position is near the bottom, it is random bedrock or stone
                        }
                        else if (y < rHeightMap[x, z] - 3)
                        {
                            rBlockMap[x, y, z] = rBlocks[1]; //If the position is lower than 3 below rHeightMap, it is stone
                        }
                        else
                        {
                            rBlockMap[x, y, z] = rBlocks[3]; //Otherwise, it is dirt
                        }
                    }
                    if (rHeightMap[x, z] < (9f / 20) * vDimensions.Y)
                    {
                        for (int y = (int)((9 / 20f) * vDimensions.Y); y > rHeightMap[x, z]; y--)
                        {
                            rBlockMap[x, y, z] = rBlocks[8]; //If any spaces below 9/20ths the total height are empty, fill them with water
                        }
                    }
                }
            }
        }

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            graphics.PreferredBackBufferWidth = 1920;
            graphics.PreferredBackBufferHeight = 1080;
            Window.Title = "Luce's XNA Project";
            graphics.ApplyChanges();

            //Set mouse to the middle
            Mouse.SetPosition(1920 / 2, 1080 / 2);

            base.Initialize();
        }

        protected override void LoadContent()
        {
            //Set some graphics stuff, load the font
            spriteBatch = new SpriteBatch(GraphicsDevice);
            device = graphics.GraphicsDevice;
            aspectRatio = device.Viewport.AspectRatio;
            myFont = Content.Load<SpriteFont>("SpriteFont1");

            //Set the dimensions
            vDimensions = new Vector3(256, 128, 256);

            //Initialize these vars
            rBlocks = new Block[47];
            rBlockMap = new Block[(int)vDimensions.X, (int)vDimensions.Y, (int)vDimensions.Z];

            rBoundingBools = new bool[(int)vDimensions.X, (int)vDimensions.Y, (int)vDimensions.Z];
            rBoundingBoxes = new BoundingBox[(int)vDimensions.X, (int)vDimensions.Y, (int)vDimensions.Z];

            //Load the effect file and the textures
            effect = Content.Load<Effect>("Effect");

            tTerrain = Content.Load<Texture2D>("terrain");
            tGui = Content.Load<Texture2D>("gui");
            tWaterOverlay = Content.Load<Texture2D>("water");
            tVignette = Content.Load<Texture2D>("vignette");
            tMoon = Content.Load<Texture2D>("moon");
            tSun = Content.Load<Texture2D>("sun");
            tIcons = Content.Load<Texture2D>("icons");

            //Run the block type creation method and the block map creation method
            CreateBlockTypes();

            SetBlockMap();

            //Initialize the chunk array
            rChunks = new Chunk[(int)(vDimensions.X / 16), (int)(vDimensions.Y / 16), (int)(vDimensions.Z / 16)];

            for (int x = 0; x < (int)(vDimensions.X / 16); x++)
            {
                for (int y = 0; y < (int)(vDimensions.Y / 16); y++)
                {
                    for (int z = 0; z < (int)(vDimensions.Z / 16); z++)
                    {
                        rChunks[x, y, z] = new Chunk(new Vector3(x * 16, y * 16, z * 16), this); //Create each chunk with its position and pass it the game object
                    }
                }
            }

            player = new Player(this); //Initialize player with game object

            UpdateChunks(); //Run initial chunk update method to create vertices, etc.

            //Create array of vertices for drawing sun/moon, it's just a simple square that I transform later to move/rotate
            sunArray = new VertexPositionTexture[6];
            sunArray[0] = new VertexPositionTexture(new Vector3(-0.5f, 0, -0.5f), new Vector2(0, 0));
            sunArray[1] = new VertexPositionTexture(new Vector3(0.5f, 0, -0.5f), new Vector2(1, 0));
            sunArray[2] = new VertexPositionTexture(new Vector3(-0.5f, 0, 0.5f), new Vector2(0, 1));
            sunArray[3] = new VertexPositionTexture(new Vector3(0.5f, 0, -0.5f), new Vector2(1, 0));
            sunArray[4] = new VertexPositionTexture(new Vector3(0.5f, 0, 0.5f), new Vector2(1, 1));
            sunArray[5] = new VertexPositionTexture(new Vector3(-0.5f, 0, 0.5f), new Vector2(0, 1));

            //Set unchanging effect parameters (Fog and a constant value used for lighting)
            effect.Parameters["FogEnabled"].SetValue(true);
            effect.Parameters["FogStart"].SetValue(128);
            effect.Parameters["FogEnd"].SetValue(160);
            effect.Parameters["WorldInverseTranspose"].SetValue(Matrix.Transpose(Matrix.Invert(Matrix.Identity)));

        }

        protected override void UnloadContent()
        {

        }

        protected override void Update(GameTime gameTime)
        {
            //Time increases at rate of 1 ingame day/night cycle per 20 minutes (actual value is 0 at dawn, 0.5pi at noon, pi at dusk, 1.5pi at midnight, and 0 or 2pi at dawn again)
            fTime += (float)(Math.PI / 36000);
            fTime %= (float)(MathHelper.TwoPi);

            //Framerate measurement
            fElapsedTime += (float)gameTime.ElapsedGameTime.TotalMilliseconds;
            if (fElapsedTime > 1000f)
            {
                nFps = nTotalFrames;
                nTotalFrames = 0;
                fElapsedTime = 0f;
            }

            //Update chunks to load close ones, unload far ones
            UpdateChunks();

            //Get mouse and keyboard state, pass them to player in the update method
            MouseState mouseState = Mouse.GetState();
            KeyboardState keyState = Keyboard.GetState();
            player.Update(mouseState, keyState);

            //Reduce nSpam, an int for stoping block placement or destroying spam
            nSpam = (nSpam > 0 ? nSpam - 1 : nSpam);

            //Check if backspace or escape are pushed, if so, exit
            if (keyState.IsKeyDown(Keys.Back) || keyState.IsKeyDown(Keys.Escape))
            {
                this.Exit();
            }

            //If left button and spam var is 0, set spamvar to be 15 (1/4 second delay), check if target block isn't bedrock, if not, then destroy it
            if (mouseState.LeftButton == ButtonState.Pressed && nSpam == 0)
            {
                nSpam = 15;
                if (player.vAim != new Vector3(-10, -10, -10))
                {
                    if (rBlockMap[(int)player.vAimBlock.X, (int)player.vAimBlock.Y, (int)player.vAimBlock.Z] != rBlocks[7])
                    {
                        SetBlock(player.vAimBlock, 0);
                    }
                }
            }

            //If right button and spam var is 0, set spamvar to be 15 and set the block coming off of the face you are looking at to be the selected block
            if (mouseState.RightButton == ButtonState.Pressed && nSpam == 0)
            {
                if (player.vAim != new Vector3(-10, -10, -10))
                {
                    nSpam = 15;
                    SetBlock(player.GetFacingBlock(), nSelectedBlock);
                }
            }

            //Scroll wheel selection stuff
            nSelectedBlock -= (int)(mouseState.ScrollWheelValue / 120) - nLastMouseWheel;
            if (nSelectedBlock <= 0)
            {
                nSelectedBlock = rBlocks.Length - 1;
            }
            else if (nSelectedBlock > rBlocks.Length - 1)
            {
                nSelectedBlock = 1;
            }
            nLastMouseWheel = (int)(mouseState.ScrollWheelValue / 120);

            //Update the player's camera
            player.cam.Update(gameTime);

            base.Update(gameTime);
        }

        public void SetBlock(Vector3 pos, int type)
        {
            if (rBlockMap[(int)pos.X, (int)pos.Y, (int)pos.Z] != rBlocks[type]) //If the target block isn't already what you want it to be
            {
                rBlockMap[(int)pos.X, (int)pos.Y, (int)pos.Z] = rBlocks[type]; //Then set the block to be that type
                rChunks[(int)Math.Floor((pos.X) / 16), (int)Math.Floor((pos.Y) / 16), (int)Math.Floor((pos.Z) / 16)].CreateVertices(); //Update current chunk
                try //Update surrounding chunks if needed
                {
                    if (Math.Round(pos.X + 1) % 16 == 0)
                    {
                        rChunks[(int)Math.Floor((pos.X) / 16) + 1, (int)Math.Floor((pos.Y) / 16), (int)Math.Floor((pos.Z) / 16)].CreateVertices();
                    }
                    else if (Math.Round(pos.X + 1) % 16 == 1)
                    {
                        rChunks[(int)Math.Floor((pos.X) / 16) - 1, (int)Math.Floor((pos.Y) / 16), (int)Math.Floor((pos.Z) / 16)].CreateVertices();
                    }
                    if (Math.Round(pos.Z + 1) % 16 == 0)
                    {
                        rChunks[(int)Math.Floor((pos.X) / 16), (int)Math.Floor((pos.Y) / 16), (int)Math.Floor((pos.Z) / 16) + 1].CreateVertices();
                    }
                    else if (Math.Round(pos.Z + 1) % 16 == 1)
                    {
                        rChunks[(int)Math.Floor((pos.X) / 16), (int)Math.Floor((pos.Y) / 16), (int)Math.Floor((pos.Z) / 16) - 1].CreateVertices();
                    }
                    if (Math.Round(pos.Y + 1) % 16 == 0)
                    {
                        rChunks[(int)Math.Floor((pos.X) / 16), (int)Math.Floor((pos.Y) / 16) + 1, (int)Math.Floor((pos.Z) / 16)].CreateVertices();
                    }
                    else if (Math.Round(pos.Y + 1) % 16 == 1)
                    {
                        rChunks[(int)Math.Floor((pos.X) / 16), (int)Math.Floor((pos.Y) / 16) - 1, (int)Math.Floor((pos.Z) / 16)].CreateVertices();
                    }
                }
                catch { }
            }
        }

        public void UpdateChunks()
        {
            //This method decides which chunks to load or not, based off of distance from player
            Chunk curchunk;
            for (int x = 0; x < (int)(vDimensions.X / 16); x++)
            {
                for (int y = 0; y < (int)(vDimensions.Y / 16); y++)
                {
                    for (int z = 0; z < (int)(vDimensions.Z / 16); z++)
                    {
                        curchunk = rChunks[x, y, z];
                        if (Vector3.Distance(new Vector3(x * 16, y * 16, z * 16), player.vPosition) < 192) //If the current chunk selected is within the distance
                        {
                            if (!curchunk.bDraw) //And if it isn't already loaded
                            {
                                curchunk.CreateVertices(); //Load it
                                curchunk.bDraw = true;
                            }
                        }
                        else
                        {
                            if (curchunk.bDraw) //Otherwise if it is out of the distance and it IS loaded
                            {
                                curchunk.lVertices = null; //Unload it
                                curchunk.buffer = null;
                                curchunk.bDraw = false;
                            }
                        }
                    }
                }
            }
        }

        protected override void Draw(GameTime gameTime)
        {
            //Framerate measurement
            nTotalFrames++;

            //Set the void color and the fog color (based off of the time of day)
            device.Clear(ClearOptions.Target, Color.SkyBlue.ToVector4() * (float)(MathHelper.Clamp((float)Math.Sin(fTime) * 5 + 1f, 0, 1)), 1f, 1);
            effect.Parameters["FogColor"].SetValue(Color.SkyBlue.ToVector4() * (float)(MathHelper.Clamp((float)Math.Sin(fTime) * 5 + 1f, 0, 1)));

            //Set some draw things
            device.DepthStencilState = DepthStencilState.None;
            device.BlendState = BlendState.AlphaBlend;
            device.SamplerStates[0] = SamplerState.PointWrap;
            device.DepthStencilState = new DepthStencilState
            {
                StencilEnable = true,
                StencilFunction = CompareFunction.GreaterEqual,
                ReferenceStencil = 254,
                DepthBufferEnable = true
            };

            RasterizerState rs = new RasterizerState();
            rs.CullMode = CullMode.CullCounterClockwiseFace;
            device.RasterizerState = rs;
            
            //CHUNKS
                //Select a rendering technique from the effect file
            effect.CurrentTechnique = effect.Techniques["Technique1"];
                //Set the view and projection matrices, as well as the texture
            effect.Parameters["View"].SetValue(player.cam.View);
            effect.Parameters["Projection"].SetValue(player.cam.Projection);
            effect.Parameters["myTexture"].SetValue(tTerrain);

                //Set lighting variables based off of time of day
            effect.Parameters["DiffuseColor"].SetValue(new Vector4(0.5f, 0.5f, 0.5f, 1) + new Vector4(0.5f, 0.5f, 0.5f, 0) * (float)(MathHelper.Clamp((float)Math.Sin(fTime) * 5, -1, 1)));
            effect.Parameters["Direction"].SetValue(new Vector3((float)(1 + ((fTime + MathHelper.PiOver2) % MathHelper.TwoPi) * (-1 / Math.PI)), (float)-(Math.Abs(1 + ((fTime + MathHelper.PiOver2) % MathHelper.TwoPi) * (-1 / Math.PI)) - 1) / 2, 0f));
            effect.Parameters["AmbientColor"].SetValue(new Vector4(0.25f, 0.25f, 0.25f, 1) + new Vector4(0.2f, 0.2f, 0.2f, 0) * (float)(MathHelper.Clamp((float)Math.Sin(fTime) * 5, -1, 1)));

            foreach (EffectPass pass in effect.CurrentTechnique.Passes) //For each pass in the current technique
            {
                foreach (Chunk curchunk in rChunks) //And for each chunk
                {
                    if (curchunk.bDraw && !curchunk.bEmpty) //If the chunk is loaded and it isn't empty
                    {
                        effect.Parameters["World"].SetValue(Matrix.CreateTranslation(curchunk.vPosition)); //Transform it to a world position
                        pass.Apply();
                        device.SetVertexBuffer(curchunk.buffer); //Load its data from its buffer
                        device.DrawPrimitives(PrimitiveType.TriangleList, 0, curchunk.buffer.VertexCount / 3); //Draw it
                    }
                }
            }

            effect.CurrentTechnique = effect.Techniques["Technique2"]; //Switch to technique 2 (gui and skybox)

            //SUN
                //Set the sun texture and world matrix (which transforms its position and angle based off of the time of day
            effect.Parameters["myTexture"].SetValue(tSun);
            effect.Parameters["World"].SetValue(Matrix.CreateScale(50) * Matrix.CreateFromYawPitchRoll(0, 0, fTime + MathHelper.PiOver2) * Matrix.CreateTranslation(player.cam.Position + new Vector3((float)(Math.Cos(fTime) * 192), (float)(Math.Sin(fTime) * 192), 0)));
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();
                device.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, sunArray, 0, 2); //Draw it

            }
            //MOON
                //Set the texture, set the world matrix to be the same thing as the sun, but negated
            effect.Parameters["myTexture"].SetValue(tMoon);
            effect.Parameters["World"].SetValue(Matrix.CreateScale(50) * Matrix.CreateFromYawPitchRoll(0, 0, (float)((fTime + MathHelper.PiOver2) + Math.PI)) * Matrix.CreateTranslation(player.cam.Position - new Vector3((float)(Math.Cos(fTime) * 192), (float)(Math.Sin(fTime) * 192), 0)));
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();
                device.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, sunArray, 0, 2); //Draw it (the same vertex data can be used because it's still just a square

            }

            //FACE MARKER
                //Set world matrix to be default
            effect.Parameters["World"].SetValue(Matrix.Identity);
                //Set the texture to be the gui texture
            effect.Parameters["myTexture"].SetValue(tGui);

            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();
                device.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, player.rFaceMarker, 0, 2); //Draw the face marker
            }

            //////////////////////
            //   START THE 2D   //
            //////////////////////

            spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.Default, RasterizerState.CullNone); //Start 2d rendering
            if (player.bUnderwater) //If underwater, apply several overlays
            {
                spriteBatch.Draw(tWaterOverlay, new Rectangle(0, 0, device.DisplayMode.Width, device.DisplayMode.Height), Color.DarkBlue);
                spriteBatch.Draw(tWaterOverlay, new Rectangle(0, 0, device.DisplayMode.Width, device.DisplayMode.Height), Color.White);
                spriteBatch.Draw(tVignette, new Rectangle(0, 0, device.DisplayMode.Width, device.DisplayMode.Height), Color.White);
            }
            spriteBatch.End();

            //Refresh spritebatch (it fixed some bugs I was encountering)
            spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.Default, RasterizerState.CullNone);

            for (int i = 1; i < rBlocks.Length; i++)
            { //This draws the block types at the bottom of the screen
                spriteBatch.Draw(tTerrain, new Rectangle((device.DisplayMode.Width / 8) + (i - 1) * (int)(device.DisplayMode.Width / ((8/6f) * (rBlocks.Length - 1))), device.DisplayMode.Height - 96, 32 - (int)(Math.Abs(i - nSelectedBlock) * 0.75), 32 - (int)(Math.Abs(i - nSelectedBlock) * 0.75)), new Rectangle((int)(rBlocks[i].UVMapTop.X * 256), (int)(rBlocks[i].UVMapTop.Y * 256), 16, 16), (i == nSelectedBlock ? Color.White : Color.Gray));
            }

            for (int i = 0; i < (int)(player.nHealth/4f); i++)
            {
                spriteBatch.Draw(tIcons, new Rectangle(100 + i * 48, (device.DisplayMode.Height - 200), 32, 32), new Rectangle(0, 0, 16, 16), Color.White);
            }

            spriteBatch.Draw(tIcons, new Rectangle(100 + 48 * (int)(player.nHealth/4f), (device.DisplayMode.Height - 200), 32, 32), new Rectangle((player.nHealth % 4) * 16, 0, 16, 16), Color.White);

            for (int i = (int)(player.nHealth / 4f + 1); i < 5; i++)
            {
                spriteBatch.Draw(tIcons, new Rectangle(100 + i * 48, (device.DisplayMode.Height - 200), 32, 32), new Rectangle(64, 0, 16, 16), Color.White);
            }

            //Fun game info!
            if (player.bDebug)
            {
                spriteBatch.DrawString(myFont, "FPS: " + nFps.ToString(), new Vector2(1820, 10), Color.White);
                spriteBatch.DrawString(myFont, "Position: " + player.vPosition.ToString(), new Vector2(10, 10), Color.White);
                spriteBatch.DrawString(myFont, "Angle: " + player.cam.Angle.ToString() + ", " + Math.Round(player.cam.Angle.Z, 3).ToString(), new Vector2(10, 30), Color.White);
                spriteBatch.DrawString(myFont, "Dimensions: " + vDimensions.ToString(), new Vector2(10, 50), Color.White);
                spriteBatch.DrawString(myFont, "Selected block: " + nSelectedBlock.ToString(), new Vector2(10, 70), Color.White);
                spriteBatch.DrawString(myFont, "Player Velocity: " + player.vPlayerVel.ToString(), new Vector2(10, 90), Color.White);
                spriteBatch.DrawString(myFont, "Aim Position: " + player.vAim.ToString(), new Vector2(10, 110), Color.White);
                spriteBatch.DrawString(myFont, "Light Direction: " + new Vector3((float)(vDimensions.X / 2 + Math.Cos(fTime) * vDimensions.X / 2), (float)(vDimensions.Y / 2 + Math.Sin(fTime) * vDimensions.Y / 2), (float)(vDimensions.Z / 2)).ToString(), new Vector2(10, 130), Color.White);
                spriteBatch.DrawString(myFont, "Time: " + (24 * ((fTime + MathHelper.PiOver2) % MathHelper.TwoPi) / MathHelper.TwoPi).ToString(), new Vector2(10, 150), Color.White);
            }

            //Cursor!
            spriteBatch.Draw(tGui, new Rectangle(device.DisplayMode.Width / 2 - 16, device.DisplayMode.Height / 2 - 16, 32, 32), new Rectangle(0, 0, 32, 32), Color.White);

            spriteBatch.End();

            base.Draw(gameTime);
        }

        static int IntClamp(int Value, int Min, int Max)
        {
            //Clamp an int (the default clamp requires conversions to/from float)
            return (Value < Min ? Min : (Value > Max ? Max : Value));
        }

        //Terrain generation, don't even try to touch this

        public class PerlinGenerator
        {
            private const int GradientSizeTable = 256;
            private readonly Random _random;
            private readonly float[] _gradients = new float[GradientSizeTable * 3];

            private readonly byte[] _perm = new byte[] {
  225,155,210,108,175,199,221,144,203,116, 70,213, 69,158, 33,252,
  5, 82,173,133,222,139,174, 27,  9, 71, 90,246, 75,130, 91,191,
  169,138,  2,151,194,235, 81,  7, 25,113,228,159,205,253,134,142,
  248, 65,224,217, 22,121,229, 63, 89,103, 96,104,156, 17,201,129,
  36,  8,165,110,237,117,231, 56,132,211,152, 20,181,111,239,218,
  170,163, 51,172,157, 47, 80,212,176,250, 87, 49, 99,242,136,189,
  162,115, 44, 43,124, 94,150, 16,141,247, 32, 10,198,223,255, 72,
  53,131, 84, 57,220,197, 58, 50,208, 11,241, 28,  3,192, 62,202,
  18,215,153, 24, 76, 41, 15,179, 39, 46, 55,  6,128,167, 23,188,
  106, 34,187,140,164, 73,112,182,244,195,227, 13, 35, 77,196,185,
  26,200,226,119, 31,123,168,125,249, 68,183,230,177,135,160,180,
  12,  1,243,148,102,166, 38,238,251, 37,240,126, 64, 74,161, 40,
  184,149,171,178,101, 66, 29, 59,146, 61,254,107, 42, 86,154,  4,
  236,232,120, 21,233,209, 45, 98,193,114, 78, 19,206, 14,118,127,
  48, 79,147, 85, 30,207,219, 54, 88,234,190,122, 95, 67,143,109,
  137,214,145, 93, 92,100,245,  0,216,186, 60, 83,105, 97,204, 52};

            public PerlinGenerator(int seed)
            {
                _random = new Random(seed);
                InitGradients();
            }

            public float Noise(float x, float y, float z)
            {
                int ix = (int)Math.Floor(x);
                float fx0 = x - ix;
                float fx1 = fx0 - 1;
                float wx = Smooth(fx0);

                int iy = (int)Math.Floor(y);
                float fy0 = y - iy;
                float fy1 = fy0 - 1;
                float wy = Smooth(fy0);

                int iz = (int)Math.Floor(z);
                float fz0 = z - iz;
                float fz1 = fz0 - 1;
                float wz = Smooth(fz0);

                float vx0 = Lattice(ix, iy, iz, fx0, fy0, fz0);
                float vx1 = Lattice(ix + 1, iy, iz, fx1, fy0, fz0);
                float vy0 = Lerp(wx, vx0, vx1);

                vx0 = Lattice(ix, iy + 1, iz, fx0, fy1, fz0);
                vx1 = Lattice(ix + 1, iy + 1, iz, fx1, fy1, fz0);
                float vy1 = Lerp(wx, vx0, vx1);

                float vz0 = Lerp(wy, vy0, vy1);

                vx0 = Lattice(ix, iy, iz + 1, fx0, fy0, fz1);
                vx1 = Lattice(ix + 1, iy, iz + 1, fx1, fy0, fz1);
                vy0 = Lerp(wx, vx0, vx1);

                vx0 = Lattice(ix, iy + 1, iz + 1, fx0, fy1, fz1);
                vx1 = Lattice(ix + 1, iy + 1, iz + 1, fx1, fy1, fz1);
                vy1 = Lerp(wx, vx0, vx1);

                float vz1 = Lerp(wy, vy0, vy1);
                return Lerp(wz, vz0, vz1);
            }

            private void InitGradients()
            {
                for (int i = 0; i < GradientSizeTable; i++)
                {
                    float z = 1f - 2f * (float)_random.NextDouble();
                    float r = (float)Math.Sqrt(1f - z * z);
                    float theta = 2 * (float)Math.PI * (float)_random.NextDouble();
                    _gradients[i * 3] = r * (float)Math.Cos(theta);
                    _gradients[i * 3 + 1] = r * (float)Math.Sin(theta);
                    _gradients[i * 3 + 2] = z;
                }
            }

            private int Permutate(int x)
            {
                const int mask = GradientSizeTable - 1;
                return _perm[x & mask];
            }

            private int Index(int ix, int iy, int iz)
            {
                return Permutate(ix + Permutate(iy + Permutate(iz)));
            }

            private float Lattice(int ix, int iy, int iz, float fx, float fy, float fz)
            {
                int index = Index(ix, iy, iz);
                int g = index * 3;
                return _gradients[g] * fx + _gradients[g + 1] * fy + _gradients[g + 2] * fz;
            }

            private float Lerp(float t, float value0, float value1)
            {
                return value0 + t * (value1 - value0);
            }

            private float Smooth(float x)
            {
                return x * x * (3 - 2 * x);
            }
        }

        public class HeightMap
        {
            public float[,] Heights { get; set; }
            public int Size { get; set; }

            public HeightMap(int size)
            {
                Size = size;
                Heights = new float[Size, Size];
                Perlin = new PerlinGenerator(new Random().Next(-50000, 50000));
                AddPerlinNoise(6f);
                Perturb(10, 10);
                for (int i = 0; i < 10; i++)
                {
                    Erode(8);
                }
                Smoothen();
            }

            private PerlinGenerator Perlin { get; set; }

            public void AddPerlinNoise(float f)
            {
                for (int i = 0; i < Size; i++)
                {
                    for (int j = 0; j < Size; j++)
                    {
                        Heights[i, j] += Perlin.Noise(f * i / (float)Size, f * j / (float)Size, 0);
                    }
                }
            }

            public void Perturb(float f, float d)
            {
                int u, v;
                float[,] temp = new float[Size, Size];
                for (int i = 0; i < Size; ++i)
                {
                    for (int j = 0; j < Size; ++j)
                    {
                        u = i + (int)(Perlin.Noise(f * i / (float)Size, f * j / (float)Size, 0) * d);
                        v = j + (int)(Perlin.Noise(f * i / (float)Size, f * j / (float)Size, 1) * d);
                        if (u < 0) u = 0; if (u >= Size) u = Size - 1;
                        if (v < 0) v = 0; if (v >= Size) v = Size - 1;
                        temp[i, j] = Heights[u, v];
                    }
                }
                Heights = temp;
            }

            public void Erode(float smoothness)
            {
                for (int i = 1; i < Size - 1; i++)
                {
                    for (int j = 1; j < Size - 1; j++)
                    {
                        float d_max = 0.0f;
                        int[] match = { 0, 0 };

                        for (int u = -1; u <= 1; u++)
                        {
                            for (int v = -1; v <= 1; v++)
                            {
                                if (Math.Abs(u) + Math.Abs(v) > 0)
                                {
                                    float d_i = Heights[i, j] - Heights[i + u, j + v];
                                    if (d_i > d_max)
                                    {
                                        d_max = d_i;
                                        match[0] = u; match[1] = v;
                                    }
                                }
                            }
                        }

                        if (0 < d_max && d_max <= (smoothness / (float)Size))
                        {
                            float d_h = 0.5f * d_max;
                            Heights[i, j] -= d_h;
                            Heights[i + match[0], j + match[1]] += d_h;
                        }
                    }
                }
            }

            public void Smoothen()
            {
                for (int i = 1; i < Size - 1; ++i)
                {
                    for (int j = 1; j < Size - 1; ++j)
                    {
                        float total = 0.0f;
                        for (int u = -1; u <= 1; u++)
                        {
                            for (int v = -1; v <= 1; v++)
                            {
                                total += Heights[i + u, j + v];
                            }
                        }

                        Heights[i, j] = total / 9.0f;
                    }
                }
            }
        }
    }
}
